using System;
using NUnit.Framework;
using NUnit.Framework.Constraints;
using Tycho.Grammar;
using Tycho.Language;
using Tycho.Parser.Peg;

namespace Tycho.UnitTests.Grammar {
    [TestFixture]
    public class GIVEN_grammar_syntax {
        [Test]
        public void WHEN_single_production_is_specified_THEN_it_is_used_to_parse_source () {
            IParser grammar = CompileGrammar (@"plus {a: int '+' b: int}");
            var term = grammar.ParseTerm ("1 + 2");
            term.ShouldBe ("plus {a: 1, b: 2}");
        }

        [Test]
        public void WHEN_production_is_given_name_AND_that_name_is_last_THEN_use_that_named_production_to_parse_source () {
            string source =
@"y = plus {a: int '+' b: int}
y";

            IParser grammar = CompileGrammar (source);
            var term = grammar.ParseTerm ("1 + 2");
            term.ShouldBe ("plus {a: 1, b: 2}");
        }

        [Test]
        public void WHEN_2_productions_are_given_names_AND_one_of_those_names_is_last_THEN_use_that_named_production_to_parse_source () {
            string source =
@"x = times {a: int '*' b: int}
y = plus {a: int '+' b: int}
x";

            IParser grammar = CompileGrammar (source);
            var term = grammar.ParseTerm ("3 * 4");
            term.ShouldBe ("times {a: 3, b: 4}");
        }

        [Test]
        public void WHEN_2_productions_are_given_names_AND_one_of_those_uses_the_other_THEN_it_should_use_that_to_parse () {
            string source =
@"x = times {a: int '*' b: int}
y = plus {a: int '+' b: x}
y";

            IParser grammar = CompileGrammar (source);
            var term = grammar.ParseTerm ("1 + 3 * 4");
            term.ShouldBe ("plus {a: 1, b: times {a: 3, b: 4}}");
        }

        [Test]
        public void WHEN_productions_use_production_names_from_outside_grammar_THEN_they_should_be_resolved_and_used () {
            var source = "plus {left: id '+' right: id}";

            IParser grammar = CompileGrammar (source);

            var term = grammar.ParseTerm ("a + b");
            term.ShouldBe ("plus {left: a, right: b}");
        }

        [Test]
        public void WHEN_choice_production_is_used_THEN_either_option_should_be_parsed () {
            var source =
@"atom = choice {int, id}
plus {left: id '+' right: atom}";

            IParser grammar = CompileGrammar (source);

            var term = grammar.ParseTerm ("a + b");
            term.ShouldBe ("plus {left: a, right: b}");

            term = grammar.ParseTerm ("a + 3");
            term.ShouldBe ("plus {left: a, right: 3}");
        }

        [Test]
        public void WHEN_last_production_has_name_THEN_it_is_used_as_last_production_even_if_name_not_used () {
            IParser grammar = CompileGrammar (@"plus = plus {a: int '+' b: int}");
            var term = grammar.ParseTerm ("1 + 2");
            term.ShouldBe ("plus {a: 1, b: 2}");
        }

        [Test]
        public void WHEN_non_terminal_contains_reference_to_itself_THEN_it_should_be_recursive () {
            var source = "recurse = plus {a: int '+' b: recurse / a: int}";
            IParser parser = CompileGrammar (source);
            ITerm term = parser.ParseTerm ("1 + 2 + 3 + 4");
            term.ShouldBe ("plus {a: [1], b: plus {a: [2], b: plus {a: [3], b: plus {a: [4]}}}}");
        }

        [Test]
        public void WHEN_two_productions_refer_to_each_other_THEN_they_should_be_mutually_recursive () {
            var source =
@"plus = plus {a: id '+' b: expression}
expression = choice {plus, id}";

            IParser parser = CompileGrammar (source);
            ITerm term = parser.ParseTerm ("a + b + c + d");
            term.ShouldBe ("plus {a: a, b: plus {a: b, b: plus {a: c, b: d}}}");
        }

        [Test]
        public void WHEN_a_choice_references_a_production_that_is_defined_later_THEN_the_choice_should_contain_that_production () {
            var source =
@"c = choice {prod, id, int}
prod = ret {'ret' expr: c}";

            IParser parser = CompileGrammar (source);
            ITerm term = parser.ParseTerm ("ret ret 5");
            term.ShouldBe ("ret {expr: ret {expr: 5}}");
        }

        [Test]
        public void WHEN_a_term_macro_is_used_THEN_when_that_production_is_parsed_the_term_should_be_returned () {
            var source =
@"macro {'macro' name: id} => term stuff {macro: #name}";

            IParser parser = CompileGrammar (source);
            ITerm term = parser.ParseTerm ("macro notion");
            term.ShouldBe ("stuff {macro: notion}");
        }

        [Test]
        public void WHEN_operator_precedence_is_defined_in_PEG_THEN_it_should_work () {
            var source =
@"value = int

power = choice
  power {left: power '^' right: value}
  value

multiply = choice
  multiply {left: power '*' right: multiply}
  power
";

            IParser parser = CompileGrammar (source);
            ITerm term = parser.ParseTerm ("1 ^ 2");
            term.ShouldBe ("power {left: 1, right: 2}");
        }

        [Test]
        public void WHEN_dynamic_choice_has_two_infix_operators_THEN_the_first_operator_should_have_lower_precedence () {
            var source =
@"expression = choice
  not {'!' right: expression}
  plus {left: expression '+' right: expression}
  multiply {left: expression '*' right: expression}
  id";

            IParser parser = CompileGrammar (source);
            ITerm term = parser.ParseTerm ("x * y + z");
            term.ShouldBe ("plus {left: multiply {left: x, right: y}, right: z}");
            term = parser.ParseTerm ("x + y * z");
            term.ShouldBe("plus {left: x, right: multiply {left: y, right: z}}");
        }

        [Test]
        public void WHEN_two_productions_cause_left_recursion_at_same_index_THEN_parsing_should_handle_both () {
            var source =
@"expression = choice
    constant {target: schema '=' source: expression}
    value-expression

value-expression = choice 
    index {expression: expression '[' index: expression ']'}
    id

schema = choice
    structure {'{' fields: struct-schema-field '}'}
    value-expression

struct-schema-field = choice
    field {name: id '=' value: schema}
    id

expression
";

            IParser parser = CompileGrammar (source);
            ITerm term = parser.ParseTerm ("{f1 = a} = c");
            term.ShouldBe ("constant {target: structure {fields: field {name: f1, value: a}}, source: c}");
        }

        [Test]
        public void WHEN_dynamic_choice_has_two_infix_operator_macros_THEN_the_first_operator_should_have_lower_precedence () {
            var source =
@"expression = choice {
  macro {left: expression '+' right: expression} => term plus {left: #left, right: #right}
  macro {left: expression '*' right: expression} => term multiply {left: #left, right: #right}
  id
}";

            IParser parser = CompileGrammar (source);
            ITerm term = parser.ParseTerm ("x * y + z");
            term.ShouldBe ("plus {left: multiply {left: x, right: y}, right: z}");
        }

        [Test]
        public void WHEN_named_production_is_rule_literal_THEN_should_be_able_to_use_in_other_non_terminal () {
            var source =
@"delim = (';' / ',')
expression {a: id delim b: id}";

            IParser parser = CompileGrammar (source);
            ITerm term = parser.ParseTerm ("y ; z");
            term.ShouldBe ("expression {a: y, b: z}");
        }

        private static IParser CompileGrammar (string source) {
            return new ParserLoader ().LoadParser (source);
        }
    }
}